# Javascript ES6(一)
# 块级作用域
# let 声明
let 声明的语法与 var 的语法一致。你基本上可以用 let 来代替 var 进行变量声明,但会将变量的作用域限制在当前代码块中。
- 和 
var不一样的是let不会变量提升。 - 禁止重复声明:
如果一个标识符已经在代码块内部被定义,那么在此代码块内使用同一个标识符进行 
let声明就会导致抛出错误。var不会有这个限制。 
# const 声明
使用 const 声明的变量会被认为是常量,意味着它们的值在被设置完成后就不能再被改变。仅针对值类型,如果是引用类型,它所包含的值是可以修改的。
- 禁止重复声明:
前面无论是
var定义过,let定义过都会报错。 
# 块级绑定
# 暂时性死区
使用 let 或 const 声明的变量,若试图在定义位置之前使用它,则会出现暂时性死区。不同作用域不受此影响。
console.log(typeof value); // "undefined"
if(condition){
    console.log(typeof value); // 引用错误
    let value = 'val';
}
# 循环内的let
 在每次迭代中,都会创建一个新的同名变量并对其进行初始化。
var funcs = [];
for (let i = 0; i < 10; i++) {
    funcs.push(function() {
        console.log(i);
    });
}
funcs.forEach(function(func) {
    func(); // 从 0 到 9 依次输出
})
在循环中 let 声明每次都创建了一个新的 i 变量,因此在循环内部创建的函数获得了各自的 i 副本,而每个 i 副本的值都在每次循环迭代声明变量的时候被确定了。
要理解上面那句话只要把let i = 0; 放到循环外定义就知道了。
比如下面结果会输出10个10:
var funcs = [];
let i = 0; // 相当于切断了i的块级绑定
for (; i < 10; i++) {
    funcs.push(function() {
        console.log(i);
    });
}
funcs.forEach(function(func) {
    func(); // 10个10
})
# 循环内的 const
 - 不能在
for循环中用const定义i,因为i++会对i进行修改。 - 而另一方面, 
const变量在for-in或for-of循环中使用时,与let变量效果相同。const能够在for-in与for-of循环内工作,是因为循环为每次迭代创建了一个新的变量绑定,而不是试图去修改已绑定的变量的值。 
# 全局块级绑定
let和const定义在全局的变量不会被添加到全局对象上。这也就意味着你不能使用 let 或 const 来覆盖一个全局变量,你只能将其屏蔽。
# 最佳实践
块级绑定当前的最佳实践就是:在默认情况下使用 const ,而只在你知道变量值需要被更改
的情况下才使用 let 。这在代码中能确保基本层次的不可变性,有助于防止某些类型的错
误。
# 字符串
includes()方法,在给定文本存在于字符串中的任意位置时会返回true,否则返回false;支持第二个参数,从下标index开始查找。startsWith()方法,在给定文本出现在字符串起始处时返回true,否则返回false;支持第二个参数,从下标index开始查找。endsWith()方法,在给定文本出现在字符串结尾处时返回true,否则返回false。支持第二个参数,从下标string.length - index开始查找。repeat()方法,它接受一个参数作为字符串的重复次数,返回一个将初始字符串重复指定次数的新字符串。
# 函数
# 参数默认值
对未明确指定的参数采用默认值传入。
function makeRequest(url, timeout = 2000, callback = function(){}) {
// 函数的剩余部分
}
makeRequest('/foo'); // ==> makeRequest('/foo', 2000, function(){})
arguments只存储明确指定的参数,比如上面例子里,arguments = ['/foo']。
你可以执行一个函数来产生参数的默认值。
function getValue() {
    return 5;
}
function add(first, second = getValue()) {
    return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 6
注意这里的getValue()只有在second未传入时才会调用。而在 getValue() 的函数声明初次被解析时并不会进行调用。
也可以把前一个值当成默认参数。
function add(first, second = first) {
    return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 2
把前一个值传入函数产生当前默认值。
function getValue(value) {
    return value + 5;
}
function add(first, second = getValue(first)) {
    return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 7
引用其他参数来为参数进行默认赋值时,仅允许引用前方的参数,因此前面的参数不能访问后面的参数。这是参数默认值的暂时性死区(TDZ)概念。变量定义前不能引用。
function add(first = second, second) {
    return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(undefined, 1)); // 抛出错误
# 剩余参数
剩余参数( rest parameter )由三个点( ... )与一个紧跟着的具名参数指定,它会是包 含传递给函数的其余参数的一个数组,名称中的“剩余”也由此而来。
function add(...nums) {
    return nums.reduce((a, b) => a + b);
}
add(1,2,3) // 6
函数的 length 属性用于指示具名参数的数量,而剩余参数对其毫无影响。比如上面add.length = 0
剩余参数和arguments的值是一一对应的。比如上例nums[0] === arguments[0], nums[1] === arguments[1]。
# 剩余参数的限制条件
- 函数只能有一个剩余参数,并且它必须被放在最后。否则会报 
Rest parameter must be last formal parameter(语法错误:不能在剩余参数后使用具名参数)。 - 剩余参数不能在对象字面量的 
setter属性中使用。因为对象字面量的setter被限定只能使用单个参数;而剩余参数按照定义是不限制参数数量的。 
# 扩展运算符
剩余参数允许你把多个独立的参数合并到一个数组中;而扩展运算符则允许将一个数组分割,并将各个项作为分离的参数传给函数。这也就是说可以用到的地方就是把数组分割成项。比如:
Math.max()比较数组内最大值。
let arr = [1,2,3,8]
Math.max(...arr); // 8
拷贝数组
let arr = [1,2,3,8]
arr1 = arr
arr2 = [...arr]
arr1[0] = 9
console.log(arr[0]) // 9
console.log(arr2[0]) // 1
# 明确函数的双重用途
JS 为函数提供了两个不同的内部方法: [[Call]] 与 [[Construct]] 。当函数未使用 new进行调用时, [[call]] 方法会被执行,运行的是代码中显示的函数体。而当函数使用 new进行调用时, [[Construct]] 方法则会被执行,负责创建一个被称为新目标的新的对象,并且使用该新目标作为 this 去执行函数体。拥有 [[Construct]] 方法的函数被称为构造器。
# 在 ES5 中判断函数如何被调用
在 ES5 中判断函数是不是使用了 new 来调用(即作为构造器),最流行的方式是使用
instanceof,例如:
function Person(name) {
    if (this instanceof Person) {
        this.name = name; // 使用 new
    } else {
        throw new Error("You must use new with Person.")
    }
}
var person = new Person("Nicholas");
var notAPerson = Person("Nicholas"); // 抛出错误
可惜的是,该方法并不绝对可靠,因为在不使用 new 的情况下 this 仍然可能
是 Person 的实例
function Person(name) {
    if (this instanceof Person) {
        this.name = name; // 使用 new
    } else {
        throw new Error("You must use new with Person.")
    }
}
var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // 奏效了!
# new.target 元属性
当函数的 [[Construct]] 方法被调用时, new.target 会被填入 new 运算符的作用目标,该目标通常是新创建的对象实例的构造器,并且会成为函数体内部的 this 值。而若 [[Call]] 被执行, new.target 的值则会是undefined
安全地判断函数是否被使用 new 进行了调用.
function Person(name) {
    if (typeof new.target !== "undefined") {
        this.name = name; // 使用 new
    } else {
        throw new Error("You must use new with Person.")
    }
}
var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // 出错!
也可以检查 new.target 是否被使用特定构造器进行了调用。
function Person(name) {
    if (new.target === Person) {
        this.name = name; // 使用 new
    } else {
        throw new Error("You must use new with Person.")
    }
}
function AnotherPerson(name) {
    Person.call(this, name);
}
var person = new Person("Nicholas");
var anotherPerson = new AnotherPerson("Nicholas"); // 出错!
# 箭头函数
箭头函数正如名称所示那样,使用一个“箭头”( => )来定义,但它的行为在很多重要方面与传统的 JS 函数不同:
- 没有 
this、super、arguments,也没有new.target绑定 - 不能被使用 
new调用: 箭头函数没有[[Construct]]方法。 - 没有原型
 - 不能更改 
this - 没有 
arguments对象 - 不允许重复的具名参数
 
总结箭头函数不能定义构造函数,不能更改 this,没有 arguments 对象。其他表现跟普通函数一样。
# 没有this绑定
JS 最常见的错误领域之一就是在函数内的 this 绑定。
var PageHandler = {
    id: "123456",
    init: function() {
        document.addEventListener("click", function(event) {
            this.doSomething(event.type); // 错误 这里this指向的是document
        }, false);
    },
    doSomething: function(type) {
        console.log("Handling " + type + " for " + this.id);
    }
};
你可以明确使用 bind() 方法将函数的 this 值绑定到 PageHandler 上,以修正这段代码:
var PageHandler = {
    id: "123456",
    init: function() {
        // 这里的this是PageHandler
        document.addEventListener("click", (function(event) {
            this.doSomething(event.type); // 成功执行 this被指向到PageHandler
        }).bind(this), false);
    },
    doSomething: function(type) {
        console.log("Handling " + type + " for " + this.id);
    }
};
箭头函数:
var PageHandler = {
    id: "123456",
    init: function() {
        document.addEventListener("click",
        event => this.doSomething(event.type), false);
    },
    doSomething: function(type) {
        console.log("Handling " + type + " for " + this.id);
    }
}
箭头函数没有 this 绑定,意味着箭头函数内部的 this 值只能通过查找作用域链来确定。如果箭头函数被包含在一个非箭头函数内,那么 this 值就会与该函数的相等;否则,
this 值就会是全局对象。
同样,由于箭头函数的 this 值由包含它的函数决定,因此不能使用 call() 、 apply() 或 bind() 方法来改变其 this 值。
只是不能改变,还是可以调用。
var sum = (num1, num2) => num1 + num2;
console.log(sum.call(null, 1, 2)); // 3
console.log(sum.apply(null, [1, 2])); // 3
var boundSum = sum.bind(null, 1, 2);
console.log(boundSum()); // 3
# 尾调用优化
尾调用( tail call )指的是调用函数的语句是另一个函数的最后语句。
当满足以下条件时,尾调用优化会清除当前栈帧并再次利用它,而不是为尾调用创建新的栈帧:
- 尾调用不能引用当前栈帧中的变量(意味着该函数不能是闭包);
 - 进行尾调用的函数在尾调用返回结果后不能做额外操作;
 - 尾调用的结果作为当前函数的返回值。
 
作为一个例子,下面代码满足了全部三个条件,因此能被轻易地优化:
function doSomething() {
    // 被优化
    return doSomethingElse();
}
尾调用优化允许某些函数的调用被优化,以保持更小的调用栈、使用更少的内存,并防止堆 栈溢出。当能进行安全优化时,它会由引擎自动应用。不过你可以考虑重写递归函数,以便 能够利用这种优化。
# 扩展的对象功能
# 对象字面量语法的扩展
# 属性初始化器的速记法
在 ES5 及更早版本中,对象字面量是“键/值对”的简单集合。
function createPerson(name, age) {
    return {
        name: name,
        age: age
    };
}
在 ES6 中,当对象的一个属性名称与本地变量名相同时,你可以简单书写名称而省略冒号与值。
function createPerson(name, age) {
    return {
        name,
        age
    };
}
# 方法简写
在 ES5 及更早版本中,你必须指定一个名称, 并用完整的函数定义来为对象添加方法。
var person = {
    name: "Nicholas",
    sayName: function() {
        console.log(this.name);
    }
};
ES6
var person = {
    name: "Nicholas",
    sayName() {
        console.log(this.name);
    }
};
这种写法具备 ES5 的 sayName() 方法的所有特征。而有一点区别是:方法简写能使用 super 。
# 新的方法
# Object.is()
比较二者类型相同并且值也相等。
console.log(+0 == -0); // true
console.log(+0 === -0); // true
console.log(Object.is(+0, -0)); // false
console.log(NaN == NaN); // false
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true
console.log(5 == 5); // true
console.log(5 == "5"); // true
console.log(5 === 5); // true
console.log(5 === "5"); // false
console.log(Object.is(5, 5)); // true
console.log(Object.is(5, "5")); // false
在许多情况下, Object.is() 的结果与 === 运算符是相同的,仅有的例外是:它会认为 +0 与 -0 不相等,而且 NaN 等于 NaN 。
# Object.assign()
合并多个对象项到第一个对象。
var receiver = {};
Object.assign(receiver,
    {
        type: "js",
        name: "file.js"
    },
    {
        type: "css"
    }
);
console.log(receiver.type); // "css"
console.log(receiver.name); // "file.js"
对象有多个同项时,后一项会覆盖前一项。
# 重复的对象字面量属性
var person = {
    name: "Nicholas",
    name: "Greg" // 在 ES6 严格模式中不会出错 ES5会
};
console.log(person.name); // "Greg"
# 自有属性的枚举顺序
ES5 并没有定义对象属性的枚举顺序,而是把该问题留给了 JS 引擎厂商。而 ES6 则严格定义了对象自有属性在被枚举时返回的顺序。
这对 Object.getOwnPropertyNames() 与 Reflect.ownKeys 如何返回属性造成了影响,还同样影响了 Object.assign() 处理属性的顺序。
note: for-in 循环的枚举顺序仍未被明确规定,因为并非所有的 JS 引擎都采用相同的方式。而 Object.keys() 和 JSON.stringify() 也使用了与 for-in 一样的枚举顺序。
自有属性枚举时基本顺序如下:
- 所有的数字类型键,按升序排列。
 - 所有的字符串类型键,按被添加到对象的顺序排列。
 - 所有的符号类型键,也按添加顺序排列。
 
var obj = {
    a: 1,
    0: 1,
    c: 1,
    2: 1,
    b: 1,
    1: 1
};
obj.d = 1;
console.log(Object.getOwnPropertyNames(obj).join("")); // "012acbd"
# 修改对象的原型
ES5 添加了 Object.getPrototypeOf() 方法来从任意指定对象中获取其原型,但仍然缺少在初始化之后更改对象原型的标准方法。
ES6 通过添加 Object.setPrototypeOf() 方法而改变了这种假定,此方法允许你修改任意指定对象的原型。它接受两个参数:需要被修改原型的对象,以及将会成为前者原型的对象。
let person = {
    getGreeting() {
        return "Hello";
    }
};
let dog = {
    getGreeting() {
        return "Woof";
    }
};
// 原型为 person
let friend = Object.create(person);
console.log(friend.getGreeting()); // "Hello"
console.log(Object.getPrototypeOf(friend) === person); // true
// 将原型设置为 dog
Object.setPrototypeOf(friend, dog);
console.log(friend.getGreeting()); // "Woof"
console.log(Object.getPrototypeOf(friend) === dog); // true
# 正式的“方法”定义
在 ES6 之前,“方法”的概念从未被正式定义,它此前仅指对象的函数属性(而非数据属
性)。 ES6 则正式做出了定义:方法是一个拥有 [[HomeObject]] 内部属性的函数,此内部属性指向该方法所属的对象。
let person = {
    // 方法
    getGreeting() {
        return "Hello";
    }
};
// 并非方法
function shareGreeting() {
    return "Hi!";
}
对象里面的叫方法。